/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package date;//这个类专门负责MNIST数据集的加载和解析工作，数据加载逻辑封装在同一个类别里，便于维护

/**
 *
 * @author Raise
 */
import date.ImageData;
import java.io.*;
import java.util.*;

//读取MNIST二进制文件
//解析文件头信息
//将原始字节数据转换为 ImageData 对象，内置字节序检查与转换，提高兼容性
//数据预处理：处理数据归一化（0-255 → 0.0-1.0），简化后续的处理流程
//接口：提供简单的加载接口，隐藏复杂的二进制文件解析细节

//加载的数据集通过区分文件路径定义与明确加载方法划分了测试集和训练集，将训练集用于训练，测试集用于性能评估，避免过拟合

public class MNISTDataLoader {
    //定义训练集和测试集文件的路径
    private String trainImagePath;
    private String trainLabelPath;
    private String testImagePath;
    private String testLabelPath;

    //构造函数，初始化文件路径（使用默认文件名）
    public MNISTDataLoader() {
        this.trainImagePath = "train-images.idx3-ubyte";
        this.trainLabelPath = "train-labels.idx1-ubyte";
        this.testImagePath = "t10k-images.idx3-ubyte";
        this.testLabelPath = "t10k-labels.idx1-ubyte";
    }

    //提供清晰的接口分离的训练数据和测试数据的加载，使得调用变得更为简单，只需要调用相应方法即可，不需要知道具体的文件路径
    //加载训练数据
    public List<ImageData> loadTrainingData() throws IOException {
        return loadData(trainImagePath, trainLabelPath);
    }

    //加载测试数据
    public List<ImageData> loadTestData() throws IOException {
        return loadData(testImagePath, testLabelPath);
    }
    //加载数据的通用方法，传入图像文件路径和标签文件路径
    private List<ImageData> loadData(String imagePath, String labelPath) throws IOException {
        List<ImageData> data = new ArrayList<>();
        
        //创建数据输入流，使用DataInputStream包装FileInputStream，提供了各种读取数据类型的方法，便于读取二进制文件，比直接操作字节更方便
        DataInputStream imageStream = new DataInputStream(new FileInputStream(imagePath));
        DataInputStream labelStream = new DataInputStream(new FileInputStream(labelPath));
        //FileInputStream：低级文件输入流，用于从文件读取原始字节
        //DataInputStream：包装流，提供读取各种数据类型的方法（如 readInt(), readByte()）
        
        
        // 读取文件头 - 移除字节序转换
        int imageMagic = imageStream.readInt();   //读取魔术数字，应为2051
        int imageCount = imageStream.readInt();   //读取图像数量
        int rows = imageStream.readInt();         //读取图像高度
        int cols = imageStream.readInt();         //读取图像宽度
        
        //读取标签文件头信息
        int labelMagic = labelStream.readInt();   //读取标签魔术数字，应为2049
        int labelCount = labelStream.readInt();   //读取标签数量
        
        // 检查图像文件魔术数字是否正确，如果不正确再尝试字节序转换，提高代码的健壮性，能在不同系统上正确读取数据
        if (imageMagic != 2051) {
            System.out.println("图片文件魔数错误，尝试字节序转换...");
            imageStream.close();
            labelStream.close();
            return loadDataWithEndianSwap(imagePath, labelPath);
        }
        
        //打印加载信息
        System.out.printf("加载数据: 图片数量=%d, 尺寸=%dx%d\n", imageCount, rows, cols);
        
        
        for (int i = 0; i < imageCount; i++) {
            // 创建存储一张图像像素的数组，大小为28x28=784
            //为每张图像创建固定大小的像素数组，确保输入图像尺寸一致，符合神经网络的输入要求
            double[] pixels = new double[rows * cols];
            
            // 读取一张图像的所有像素（28x28=784个像素）
            for (int j = 0; j < rows * cols; j++) {
                // 读取一个像素值（0-255的无符号字节），并归一化到0-1范围，便于神经网络处理，避免数值过大导致梯度
                pixels[j] = imageStream.readUnsignedByte() / 255.0;
            }
            // 读取对应的标签（0-9的数字）
            int label = labelStream.readUnsignedByte();
            //创建ImageData对象并添加到列表
            data.add(new ImageData(pixels, label));
        }
        
        //及时关闭文件流，释放系统资源，表面内存泄漏
        imageStream.close();
        labelStream.close();
        return data;
    }
    
    // 备用的字节序转换方法（当默认读取的魔术数字不正确时使用）
    private List<ImageData> loadDataWithEndianSwap(String imagePath, String labelPath) throws IOException {
        List<ImageData> data = new ArrayList<>();
        
        DataInputStream imageStream = new DataInputStream(new FileInputStream(imagePath));
        DataInputStream labelStream = new DataInputStream(new FileInputStream(labelPath));
        
        // 使用字节序转换，确保在不同的系统上都能正确读取
        int imageMagic = Integer.reverseBytes(imageStream.readInt());
        int imageCount = Integer.reverseBytes(imageStream.readInt());
        int rows = Integer.reverseBytes(imageStream.readInt());
        int cols = Integer.reverseBytes(imageStream.readInt());
        
        int labelMagic = Integer.reverseBytes(labelStream.readInt());
        int labelCount = Integer.reverseBytes(labelStream.readInt());
        
        System.out.printf("加载数据(字节序转换后): 图片数量=%d, 尺寸=%dx%d\n", imageCount, rows, cols);
        
        int sampleCount = Math.min(1000, imageCount);
        for (int i = 0; i < sampleCount; i++) {
            double[] pixels = new double[rows * cols];
            for (int j = 0; j < rows * cols; j++) {
                pixels[j] = imageStream.readUnsignedByte() / 255.0;
            }
            int label = labelStream.readUnsignedByte();
            data.add(new ImageData(pixels, label));
        }
        
        imageStream.close();
        labelStream.close();
        return data;
    }
}

